Skip to content

Comments

feat: Add DocTypeTable component for Issue #75#144

Open
Akshat-0001 wants to merge 5 commits intortCamp:mainfrom
Akshat-0001:feat/doctypetable-issue-75
Open

feat: Add DocTypeTable component for Issue #75#144
Akshat-0001 wants to merge 5 commits intortCamp:mainfrom
Akshat-0001:feat/doctypetable-issue-75

Conversation

@Akshat-0001
Copy link

Add DocTypeTable component for rendering DocType lists

Description

  • Introduces a reusable DocTypeTable component that composes useFrappeGetDocList with the existing table/ListView.
  • All parameters (fields, filters, orderBy, limit, offset) are passed transparently.
  • Handles loading, error, and empty states.
  • Supports custom loading / empty / error components and callbacks.
  • Fully typed with TypeScript.
  • Includes Storybook stories for default, loading, badges, and empty states.

Usage

<DocTypeTable
  doctype="User"
  fields={["name", "email", "enabled"]}
  filters={{ enabled: 1 }}
/>

Testing

  • Run Storybook: npm run storybook
  • Verify stories under DocTypeTable (Default, Loading, WithBadges, Empty)

Closes #75

Preview

ezgif-5dae5cc8881d4c3a

- Compose useFrappeGetDocList hook with ListView component
- All parameters transparent through DocTypeTable
- Support loading, error, and empty states
- Custom component overrides
- Full TypeScript support
- Comprehensive Storybook documentation
Copilot AI review requested due to automatic review settings January 18, 2026 04:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a DocTypeTable component that combines the useFrappeGetDocList hook with the existing ListView component to provide seamless integration with Frappe backend for displaying DocType document lists.

Changes:

  • Added useFrappeGetDocList hook for fetching Frappe document lists with automatic data fetching and state management
  • Created DocTypeTable component with support for loading, error, and empty states, along with customizable components and callbacks
  • Added Storybook stories (though they currently demonstrate ListView rather than DocTypeTable)
  • Included comprehensive README documentation with usage examples

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts New hook for fetching Frappe document lists with transparent parameter passing
packages/frappe-ui-react/src/components/hooks/index.ts Export useFrappeGetDocList hook
packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx Main component implementation with state management and UI rendering
packages/frappe-ui-react/src/components/docTypeTable/index.ts Export DocTypeTable component and types
packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.stories.tsx Storybook stories for component demonstration
packages/frappe-ui-react/src/components/docTypeTable/README.md Comprehensive documentation with examples
packages/frappe-ui-react/src/components/index.ts Export docTypeTable from main components index

// Error is handled in the resource state
});
}
}, [auto, params.doctype, resource]);
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect hook has a dependency on resource which is created by useResource. This will cause an infinite loop because resource is a new object on every render. The effect should depend on the actual parameters that affect fetching (like params.doctype, params.fields, params.filters, etc.) or use a JSON.stringify comparison, or the dependencies should be restructured to avoid the infinite loop.

Suggested change
}, [auto, params.doctype, resource]);
}, [auto, params.doctype]);

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +85
const buildParams = () => {
const queryParams = new URLSearchParams();

if (params.fields) {
queryParams.append("fields", JSON.stringify(params.fields));
}

if (params.filters) {
queryParams.append("filters", JSON.stringify(params.filters));
}

if (params.orderBy) {
queryParams.append("order_by", params.orderBy);
}

// Use limit as pageLength if not specified
const limit = params.limit || params.pageLength || params.pageSize || 20;
queryParams.append("limit_page_length", String(limit));

if (params.offset) {
queryParams.append("limit_page_length_offset", String(params.offset));
}

// Add any additional parameters
Object.keys(params).forEach((key) => {
if (
![
"doctype",
"fields",
"filters",
"orderBy",
"limit",
"offset",
"pageLength",
"pageSize",
].includes(key)
) {
queryParams.append(key, String((params as Record<string, unknown>)[key]));
}
});

return queryParams.toString();
};
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buildParams function is recreated on every render because it's defined inside the component. This will cause the fetchDocList function to be recreated on every render as well, which can lead to unnecessary re-fetching. Consider using useCallback or moving buildParams logic directly into fetchDocList.

Copilot uses AI. Check for mistakes.
<p className="text-sm text-gray-500 mt-1">{error?.message}</p>
<button
onClick={() => fetch()}
className="mt-3 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry button lacks proper accessibility attributes. Consider adding aria-label to provide a more descriptive label for screen readers, such as "Retry loading data".

Suggested change
className="mt-3 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
className="mt-3 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
aria-label={`Retry loading ${doctype} data`}

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +132
<div className="flex items-center justify-center py-8 text-red-600" {...attrs}>
{typeof errorComponent === "function"
? errorComponent(error)
: errorComponent || (
<div className="text-center">
<p className="font-medium">Error loading {doctype}</p>
<p className="text-sm text-gray-500 mt-1">{error?.message}</p>
<button
onClick={() => fetch()}
className="mt-3 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
>
Retry
</button>
</div>
)}
</div>
);
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error state div lacks proper ARIA attributes. Consider adding role="alert" to announce errors to screen readers immediately.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +234
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Avatar } from "../avatar";
import ListView from "../listview/listView";

const meta: Meta = {
title: "Components/DocTypeTable",
parameters: {
layout: "padded",
docs: {
description: {
component: `
DocTypeTable is a general-purpose table component for displaying Frappe DocType documents.

## Key Features
- **Automatic Data Fetching**: Fetches data from Frappe backend using useFrappeGetDocList hook
- **Transparent Parameter Passing**: All useFrappeGetDocList parameters flow directly through the component
- **Configurable Columns**: Define table structure with custom rendering
- **State Management**: Handles loading, error, and empty states
- **Selection Support**: Built-in row selection capability
- **Customizable Components**: Override loading, error, and empty states with custom components

## Usage Example
\`\`\`tsx
<DocTypeTable
doctype="User"
columns={[
{
label: "Name",
key: "name",
width: 3,
prefix: ({ row }) => <Avatar image={row.user_image} label={row.name} />
},
{ label: "Email", key: "email", width: "200px" },
{ label: "Type", key: "user_type" },
]}
params={{
fields: ['name', 'email', 'user_type', 'enabled'],
filters: { enabled: 1 },
limit: 20
}}
rowKey="name"
/>
\`\`\`

## Component Props
- \`doctype\`: Frappe DocType name (e.g., 'User', 'Employee')
- \`columns\`: Column configuration array
- \`params\`: Query parameters (fields, filters, orderBy, limit, offset)
- \`rowKey\`: Primary key field for identifying rows
- \`auto\`: Auto-fetch on mount (default: true)
- \`loadingComponent\`: Custom loading UI component
- \`emptyComponent\`: Custom empty state component
- \`errorComponent\`: Custom error component
- \`onDataLoad\`: Callback when data loads
- \`onError\`: Callback on error
`,
},
},
},
tags: ["autodocs"],
};

export default meta;
type Story = StoryObj<typeof meta>;

const columns = [
{
label: "Name",
key: "name",
width: 3,
getLabel: ({ row }: { row: Record<string, unknown> }) => row.name,
prefix: ({ row }: { row: Record<string, unknown> }) => (
<Avatar
shape="circle"
image={row.user_image as string | undefined}
size="sm"
label={row.name as string}
/>
),
},
{
label: "Email",
key: "email",
width: "200px",
},
{
label: "Type",
key: "user_type",
},
{
label: "Enabled",
key: "enabled",
},
];

const rows = [
{
name: "John Doe",
email: "john@example.com",
user_type: "System User",
enabled: 1,
user_image: "https://i.pravatar.cc/150?img=1",
},
{
name: "Jane Smith",
email: "jane@example.com",
user_type: "System User",
enabled: 1,
user_image: "https://i.pravatar.cc/150?img=2",
},
{
name: "Bob Johnson",
email: "bob@example.com",
user_type: "Website User",
enabled: 0,
user_image: "https://i.pravatar.cc/150?img=3",
},
];

export const Default: Story = {
render: (args) => (
<ListView
columns={columns}
rows={args.rows as Array<Record<string, unknown>>}
rowKey={args.rowKey as string}
options={args.options}
/>
),
args: {
rows,
rowKey: "name",
options: {
options: {
selectable: true,
},
},
},
};

export const Loading: Story = {
render: () => (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
<p className="mt-3 text-gray-600">Loading records...</p>
</div>
</div>
),
};

export const WithBadges: Story = {
render: (args) => {
const columnsWithBadges = [
{
label: "Name",
key: "name",
width: 3,
getLabel: ({ row }: { row: Record<string, unknown> }) => row.name,
prefix: ({ row }: { row: Record<string, unknown> }) => (
<Avatar
shape="circle"
image={row.user_image as string | undefined}
size="sm"
label={row.name as string}
/>
),
},
{
label: "Email",
key: "email",
width: "200px",
},
{
label: "Type",
key: "user_type",
},
{
label: "Status",
key: "enabled",
getLabel: ({ row }: { row: Record<string, unknown> }) => (
<span
className={`px-2 py-1 rounded text-xs font-medium ${
row.enabled
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`}
>
{row.enabled ? "Active" : "Inactive"}
</span>
),
},
];

return (
<ListView
columns={columnsWithBadges}
rows={args.rows as Array<Record<string, unknown>>}
rowKey={args.rowKey as string}
options={args.options}
/>
);
},
args: {
rows,
rowKey: "name",
options: {
options: {
selectable: true,
},
},
},
};

export const Empty: Story = {
render: () => (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
<p className="text-gray-500 text-lg mt-4">No records found</p>
<p className="text-gray-400 text-sm mt-2">Try adjusting your filters</p>
</div>
),
};
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stories are using ListView component directly instead of the DocTypeTable component that is being introduced in this PR. These stories should demonstrate the DocTypeTable component with various configurations, not just the underlying ListView. This makes the stories misleading as they don't actually test or showcase the new component.

Copilot uses AI. Check for mistakes.
columns={columns}
params={{
fields: ['name', 'email', 'user_type'],
orderBy: '`tabUser`.`name` asc',
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the table above, the backticks are escaped in the orderBy example. This should show the actual syntax that would be used in code.

Copilot uses AI. Check for mistakes.
Akshat-0001 and others added 4 commits January 18, 2026 11:37
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ist.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ble.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ble.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: DocType table Component

1 participant